//
// Created by Administrator on 2024/10/9.
//

#ifndef CLIENT_HPP
#define CLIENT_HPP

#include <boost/asio.hpp>
#include <chrono>
#include <iostream>

#include "base.hpp"
#include "common.hpp"
#include "gateway.hpp"
#include "message_factory.hpp"
#include "utils.hpp"
#include "../protocol/jtt809_header.h"
#include "../protocol/jtt809_packet.h"
#include "../protocol/msgbody/bodies_pch.h"

namespace asio = boost::asio;
using asio::ip::tcp;
using boost::system::error_code;

namespace network {

class Client : public Waitable {
public:
    Client(asio::io_context& io_context, const std::string &host,
        const std::string &port, const JTT809LinkType &link_type, bool auto_connect = true)
        : socket_(io_context), io_context_(io_context),
          keep_alive_timer_(io_context), linke_type_(link_type), disconnect_timer_(io_context),
          host_(host), port_(port) {
        if (auto_connect)
            connect();
    }

    Client(asio::io_context& io_context, const JTT809LinkType &link_type)
        : socket_(io_context), io_context_(io_context),
          keep_alive_timer_(io_context), linke_type_(link_type), disconnect_timer_(io_context) {

    }

    Client(const Client &other) : socket_(other.io_context_), io_context_(other.io_context_),
          keep_alive_timer_(other.io_context_), linke_type_(other.linke_type_), disconnect_timer_(io_context_),
          host_(other.host_), port_(other.port_) {

    }

    void set_remote(const std::string &host, const std::string &port) {
        host_ = host;
        port_ = port;
    }

    void connect() {
        tcp::resolver resolver(io_context_);
        tcp::resolver::query query(host_, port_);
        const auto endpoint = resolver.resolve(query);
        try {
            asio::connect(socket_, endpoint);
            connected_ = true;
            keep_alive();
            reset_disconnect_timer();
        } catch (const std::exception &e) {
            logger->error("connection failed!");
            logger->error(e.what());
        }

        std::stringstream h;
        h << socket_.remote_endpoint();
        logger->info("Connection to host [{}] was accepted", h.str());
        start_read();
    }

    void deliver(JTT809Packet &packet) {
        const auto out = serialize(packet);
        logger->debug("Messages sent as follow\n{}", protocol::toHex(out->data, out->_size));
        error_code e;
        const auto bytes_transferred = asio::write(socket_, asio::buffer(out->data, out->_size), e);
        if (e) {
            logger->error(e.what());
            return;
        }
        logger->debug("{} bytes message has been sent successfully", bytes_transferred);
    }

    void deliver(std::string &to_write) {
        logger->debug("Messages sent as follow\n{}", to_write);
        const auto bytes_transferred = asio::write(socket_, asio::buffer(to_write));
        logger->debug("{} bytes data has been sent", bytes_transferred);
    }

    void stop() {
        std::stringstream on;
        on << socket_.remote_endpoint();
        connected_ = false;
        socket_.close();
        keep_alive_timer_.cancel();
        disconnect_timer_.cancel();
        logger->info("client on [{}] has been closed", on.str());
    }

    [[nodiscard]] bool connected() const {
        return connected_;
    }

protected:
    void get_packet(std::optional<Packet> &packet) override {
        packet.emplace(Gateway(read_buffer_->data(), read_buffer_->size()).packet());
    }

private:
    void start_read() {
        socket_.async_read_some(asio::buffer(*read_buffer_, 102400),
            std::bind(&Client::handle_read, this,
                asio::placeholders::error, asio::placeholders::bytes_transferred));
    }

    void handle_read(const error_code &ec, const size_t bytes_transferred) {
        if (!ec) {
            logger->debug("read {} bytes", bytes_transferred);
            logger->debug("read: {}", protocol::toHex(read_buffer_->data(), bytes_transferred));
            Gateway gateway(read_buffer_->data(), bytes_transferred);
            get_it(gateway.message_type());
            reset_disconnect_timer();
            start_read();
        } else if (ec == asio::error::connection_reset) {
            logger->error("client from {} close connection forcely", host_+":"+port_);
        } else if (ec == asio::error::eof) {
            logger->error("client close connection");
        } else {
            logger->error("other error:{}",ec.what());
        }
    }

    void reset_disconnect_timer() {
        disconnect_timer_.expires_after(TIME_TO_DISCONNECT);
        disconnect_timer_.async_wait(std::bind(&Client::disconnect_handler, this,
            asio::placeholders::error));
    }

    void disconnect_handler(const error_code &ec) {
        if (!ec) {
            stop();
        } else if (ec == asio::error::operation_aborted) {
            if (connected_) {
                logger->debug("disconnect timer has been reset");
            } else {
                logger->debug("disconnect timer has been canceled");
            }
        }
    }

    void keep_alive() {
        keep_alive_timer_.expires_after(TIME_TO_KEPPALIVE);
        keep_alive_timer_.async_wait(std::bind(&Client::handle_keep_alive, this,
            asio::placeholders::error));
    }

    void handle_keep_alive(const error_code &ec) {
        if (!ec) {
            std::stringstream to;
            to << socket_.remote_endpoint();
            logger->info("sending keep alive message to {}", to.str());
            deliver(*MessageFactory::keep_alive(linke_type_, false));
            keep_alive();
        } else if (ec == asio::error::operation_aborted) {
            logger->info("keep alive timer has been canceled");
        } else {
            logger->error("Unknown error!");
        }
    }
    tcp::socket socket_;
    asio::io_context &io_context_;
    asio::steady_timer keep_alive_timer_;
    asio::steady_timer disconnect_timer_;
    JTT809LinkType linke_type_;
    bool connected_ = false;
    std::string host_, port_;
    std::shared_ptr<std::vector<char>> read_buffer_ = std::make_shared<std::vector<char>>(1024);

    Logger logger = JTT809Logger::logger("Client");
};

}


#endif //CLIENT_HPP
